﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Management.Automation;
using System.Text;
using System.Web;
using System.Web.Script.Serialization;
using System.Web.Script.Services;
using System.Web.Services;
using System.Xml.Serialization;
using Cognifide.PowerShell.Shell.Host;
using Sitecore.Jobs;
using System.Web.Configuration;

namespace Cognifide.PowerShell.Console.Layouts
{
    /// <summary>
    /// Summary description for PowerShellWebService
    /// </summary>
    [WebService(Namespace = "http://tempuri.org/")]
    [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
    [System.ComponentModel.ToolboxItem(false)]
    // To allow this Web Service to be called from script, using ASP.NET AJAX, uncomment the following line. 
    [System.Web.Script.Services.ScriptService]
    public class PowerShellWebService : System.Web.Services.WebService
    {
        public const string STATUS_COMPLETE = "complete";
        public const string STATUS_PARTIAL = "partial";
        public const string STATUS_WORKING = "working";
        public const string STATUS_ERROR = "error";

        [WebMethod(EnableSession = true)]
        public object ExecuteCommand(string guid, string command)
        {
            JavaScriptSerializer serializer = new JavaScriptSerializer();
            StringBuilder output = new StringBuilder();

            if (!HttpContext.Current.Request.IsAuthenticated && !command.StartsWith("login-user",StringComparison.OrdinalIgnoreCase))
            {
                return serializer.Serialize(
                    new { result = "You need to be authenticated to use the PowerShell console. Please login to Sitecore first.", prompt = "PS >" });
            }

            if (command.StartsWith("recycle-session",StringComparison.OrdinalIgnoreCase))
            {
                RecycleSession(guid);
                return serializer.Serialize(
                    new { result = "Session recycled.", prompt = "PS >" });
            }

            var session = GetScriptSession(guid);
            try
            {
                var handle = Sitecore.Data.ID.NewID.ToString();
                var jobOptions = new JobOptions(GetJobID(guid, handle), "PowerShell", "shell", this, "RunJob", new object[] { session, command });
                jobOptions.AfterLife = new TimeSpan(0, 10, 0);
                jobOptions.ContextUser = Sitecore.Context.User;
                var scriptJob = JobManager.Start(jobOptions);
                System.Threading.Thread.Sleep(Settings.CommandWaitMillis);
                return PollCommandOutput(guid, handle);
            }
            catch (Exception ex)
            {
                return
                    serializer.Serialize(
                        new Result {
                             status = STATUS_ERROR,
                             result = output.ToString() +
                             "\r\n[[;#f00;#000]Ooops, something went wrong... Do you need assistance?]\r\n" +
                             "[[;#f00;#000]Send an email with the stack trace to adam@najmanowicz.com or contact me on Twitter @AdamNaj]\r\n\r\n" +
                            session.GetExceptionConsoleString(ex) + "\r\n",
                             prompt = string.Format("PS {0}>", session.CurrentLocation)
                        });
            }
        }

        protected void RunJob(ScriptSession session, string command)
        {
            try
            {
                session.ExecuteScriptPart(command);
            }
            catch (Exception ex)
            {
                var job = Sitecore.Context.Job;
                if (job != null)
                {
                    job.Status.Failed = true;
                    job.Status.Messages.Add("Ooops, something went wrong... Do you need assistance?");
                    job.Status.Messages.Add("Send an email with the stack trace to adam@najmanowicz.com or contact me on Twitter @AdamNaj");
                    job.Status.LogException(ex);
                }
                else
                {
                    Sitecore.Diagnostics.Log.Error("Script execution failed. Could not find command job.", ex, this);
                }
            }
        }

        [WebMethod(EnableSession = true)]
        public object PollCommandOutput(string guid, string handle)
        {
            var jsonConfig = new System.Web.Configuration.ScriptingJsonSerializationSection();
            JavaScriptSerializer serializer = new JavaScriptSerializer();

            if (!Sitecore.Context.User.IsAuthenticated)
            {
                return serializer.Serialize(
                    new { result = "You need to be authenticated to use the PowerShell console. Please login to Sitecore first.", prompt = "PS >" });
            }

            var session = GetScriptSession(guid);
            Result result = new Result();
            var scriptJob = JobManager.GetJob(GetJobID(guid, handle));
            if (scriptJob == null)
            {
                result.status = STATUS_ERROR;
                result.result = "Sorry, can't find your command result. Well this is embarassing. Try again?";
                result.prompt = string.Format("PS {0}>", session.CurrentLocation);
                session.Output.Clear();
                return serializer.Serialize(result);
            }
            if (!scriptJob.IsDone)
            {
                result.status = STATUS_WORKING;
                result.handle = handle;
                return serializer.Serialize(result);
            }
            if (scriptJob.Status.Failed)
            {
                result.status = STATUS_ERROR;
                var message = string.Join(System.Environment.NewLine, scriptJob.Status.Messages.Cast<string>().ToArray());
                result.result = "[[;#f00;#000]" + (message.Length > 0 ? message : "Command failed") + "]";
                result.prompt = string.Format("PS {0}>", session.CurrentLocation);
                session.Output.Clear();
                return serializer.Serialize(result);
            }
            result.status = STATUS_COMPLETE;
            int lines = 0;
            int buffer = Settings.SerializationSizeBuffer;
            bool partial = false;
            StringBuilder temp = new StringBuilder();
            StringBuilder output = new StringBuilder();
            foreach (var outputLine in session.Output)
            {
                outputLine.GetTerminalLine(temp);
                if ((output.Length + temp.Length + buffer) > jsonConfig.MaxJsonLength)
                {
                    session.Output.RemoveRange(0, lines);
                    partial = true;
                    break;
                }
                lines++;
                output.Append(temp.ToString());
                temp.Remove(0, temp.Length);
            }
            result.result = output.ToString();
            result.prompt = string.Format("PS {0}>", session.CurrentLocation);
            if (partial)
            {
                result.status = STATUS_PARTIAL;
            }
            else
            {
                session.Output.Clear();
            }
            HttpContext.Current.Response.ContentType = "application/json";
            return serializer.Serialize(result);
        }

        [WebMethod(EnableSession = true)]
        [ScriptMethod(UseHttpGet = false, ResponseFormat = ResponseFormat.Json)]
        public object CompleteCommand(string guid, string command)
        {
            JavaScriptSerializer serializer = new JavaScriptSerializer();

            if (!HttpContext.Current.Request.IsAuthenticated)
            {
                return serializer.Serialize("You are not authenticated and thus cannot use PowerShell console. Please login to Sitecore first.");
            }
            var session = GetScriptSession(guid);
            Collection<PSParseError> errors;
            Collection<PSToken> tokens = PSParser.Tokenize(command, out errors);
            string lastToken = tokens.Last().Content;
            string truncatedCommand = command.Substring(0, command.Length - lastToken.Length);

            session.ExecuteScriptPart(string.Format("TabExpansion \"{0}\" \"{1}\"", command, lastToken));
            string result = serializer.Serialize(session.Output.Select(l => truncatedCommand + l.Text).ToArray());
            session.Output.Clear();
            
            return result;
        }

        private static ScriptSession GetScriptSession(string guid)
        {
            lock (HttpContext.Current.Session)
            {
                ScriptSession session = HttpContext.Current.Session[guid] as ScriptSession;
                if (session == null)
                {
                    session = new ScriptSession();
                    HttpContext.Current.Session[guid] = session;
                    session.Initialize();
                }
                return session;
            }
        }

        protected string GetJobID(string sessionGuid, string handle)
        {
            return "PowerShell-" + sessionGuid + "-" + handle;
        }

        private static void RecycleSession(string guid)
        {
            ScriptSession session = HttpContext.Current.Session[guid] as ScriptSession;
            if (session != null)
            {
                HttpContext.Current.Session.Remove(guid);
                session.Dispose();
            }
        }

        private class Result
        {
            public string status;
            public string handle;
            public string result;
            public string prompt;
        }

    }

}